-
Notifications
You must be signed in to change notification settings - Fork 11
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat: CRUD API for Taxonomy Tags [FC-0036] #96
feat: CRUD API for Taxonomy Tags [FC-0036] #96
Conversation
Thanks for the pull request, @yusuf-musleh! Please note that it may take us up to several weeks or months to complete a review and merge your PR. Feel free to add as much of the following information to the ticket as you can:
All technical communication about the code itself will be done via the GitHub pull request interface. As a reminder, our process documentation is here. Please let us know once your PR is ready for our review and all tests are green. |
b9c5153
to
3129f55
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is looking good @yusuf-musleh !
Let me know when you're ready for a full review?
9c9cfe5
to
d158767
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Your view changes are looking great @yusuf-musleh , and you're correct, there's some rules/permission changes needed.
* 400 - Invalid parameters provided | ||
* 403 - Permission denied | ||
* 404 - Taxonomy not found | ||
|
||
""" | ||
|
||
permission_classes = [TagListPermissions] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@yusuf-musleh I think your best approach would be to modify (and rename) TagListPermissions
to provide a perm_map
like we did with ObjectTagObjectPermissions
.
That will enforce the oel_tagging.view_tag
, oel_tagging.change_tag
, and oel_tagging.delete_tag
rules before invoking the view methods.
But I think you're correct in your question -- these oel_tagging.*_tag
rules need to be updated to reflect the changes to the taxonomy view/change rules made by #94.
I think something like this would be sufficient?
def can_view_tag(user: UserType, tag: Tag | None = None) -> bool:
"""
Users can view tags for any taxonomy they can view.
""""
taxonomy = tag.taxonomy.cast() if (tag and tag.taxonomy) else None
return user.has_perm(
"oel_tagging.view_taxonomy",
taxonomy,
)
def can_change_tag(user: UserType, tag: Tag | None = None) -> bool:
"""
Users can change tags for any taxonomy they can modify.
""""
taxonomy = tag.taxonomy.cast() if (tag and tag.taxonomy) else None
return user.has_perm(
"oel_tagging.change_taxonomy",
taxonomy,
)
rules.add_perm("oel_tagging.add_tag", can_change_tag)
rules.add_perm("oel_tagging.change_tag", can_change_tag)
rules.add_perm("oel_tagging.delete_tag", can_change_tag)
rules.add_perm("oel_tagging.view_tag", can_view_tag)
rules.add_perm("oel_tagging.list_tag", can_view_tag)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@pomegranited Thanks for the pointers! I went ahead with what you suggested, however I faced a few issues and took me quite some time to figure them out:
- The
get_queryset
method inTaxonomyTagsView
was initially returning a list, as it does some custom population of an attributesubtags
and later serialized using theTagsForSearchSerializer
. For the updated permissions (usingperm_map
) to work,get_queryset
needs to return a queryset as it is used internally by DRF. The current solution I have (though not a huge fan of it) is getting the result list and constructing a queryset usingfilter
and then repopulating thesubtags
again if applicable. Ideally, I think the approach should be to refactor the underlying implementation of get_matching_tags to use querysets instead. I couldn't get to that today, but I thought I'd mention it here to get your thoughts. - After adding the
perms_map
forTagObjectPermissions
, I left thehas_object_permission
method in place, since it's a special case for the GET list endpoint. There is nottag
object in that case, we have the Taxonomy object, so we utilize thecan_view_taxonomy
permission for theoel_tagging.list_tag
case. It gets called in theget_taxonomy
method to and handle whether the use can view the Tags under this Taxonomy or not.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
1... For the updated permissions (using
perm_map
) to work,get_queryset
needs to return a queryset as it is used internally by DRF... Ideally, I think the approach should be to refactor the underlying implementation of get_matching_tags to use querysets instead.
I agree.. have you seen @bradenmacdonald 's #92 ? I think that PR does what you need already. We ran out of budget to complete it, but if it's needed for this work, then maybe we can complete it here?
And what you did for the permissions looks great, thank you.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@pomegranited I went through @bradenmacdonald 's #92, lots of great work there, and already does alot of the heavy lifting for the refactoring, however since there are substantial changes made in that PR (and some more work remaining there) I feel it might get pretty messy to include it as part of this PR. Would it make sense to leave the current code in this PR as is and then continue working on #92 and along with the refactoring needed here as part of a separate ticket?
Alternatively, I could copy the relevant changes from that #92 and include it as part of this PR? What do you guys think?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Would it make sense to leave the current code in this PR as is and then continue working on #92 and along with the refactoring needed here as part of a separate ticket?
Yes, I think that makes sense :)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Created openedx/modular-learning#142 for this.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Great, thanks for creating it @pomegranited !
f677f3f
to
39de6ec
Compare
openedx_tagging/core/tagging/api.py
Outdated
return taxonomy.cast().add_tag(tag, parent_tag_id, external_id) | ||
|
||
|
||
def update_tag_in_taxonomy(taxonomy: Taxonomy, tag: int, tag_value: str): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would generally prefer that we use value
to identify tags in these API methods, rather than having an int
tag ID. Same for parent_tag_id
-> parent_tag_value
and tag_ids -> list[str]
.
The reasons are:
- In Simplify Tag Models [FC-0030] #87 I found that using
value
(which the DB enforces to be a unique ID) lets us handle free-text and closed taxonomies consistently, without need different identifiers for different types of taxonomies. This reduced the overall line count and made it simpler to reason about. - This lets us use a similar argument type (value) for
add_tag_to_taxonomy
andupdate_tag_in_taxonomy
- The
tag_object
API now usesvalue
to identify tags, so I think we should be consistent with that. - My proposed optimization/simplification of getting tags emphasizes
value
overID
, and I'm still hoping I'll be able to finish that up at some point. - IDs aren't necessarily stable in the test suite, but
value
is so you can simplify your tests
But I know there are downsides as well so let me know if you guys don't like that direction.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ooh good points, @bradenmacdonald -- I agree that using value
where we can makes sense.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I see, that makes sense, I updated the implementation to use values instead of IDs. 3abce26
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We're getting there.. I've left a couple of minor comments, but would like to re-review once you've addressed Braden's suggestions.
Also fixed a few things in the add taxonomy tag rest api
Utilize Taxonomy permissions to determine permissions of Taxonomy Tags
39de6ec
to
f9e28da
Compare
979430a
to
3abce26
Compare
q_list = map(lambda n: Q(value__iexact=n), value) | ||
q_list = reduce(lambda a, b: a | b, q_list) | ||
valid = Tag.objects.filter(q_list).count() == len(value) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't understand why this validation needs to be this complicated, when the same validation on the Taxonomy.delete_tags
is much simpler?
Also, do we need to this extra validation in these serializers, since the implementation also enforces the same rules? I'm genuinely asking, I'm not sure where this should sit, but I don't think it needs to be duplicated.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The implementation in the Taxonomy.delete_tags
is actually an incorrect one, since the simple value__in
filter doesnt account for case-insensitve values, the current tests didn't catch it. Thanks for pointing it out, I'll update it.
Also, do we need to this extra validation in these serializers, since the implementation also enforces the same rules?
My initial thought process was to add the validation in the api incase the functions were called directly rather than only in the rest_api/views (that go through serializers), however then I figured we could also make use of the validation that DRF provides in addition to that.
The rules they enforce are slightly different, you'll notice the validation that happens in the serializer checks if the Tag values provided are valid (i.e exist in the DB regardless to which taxonomy they belong to) however the validation that happens in the api method checks if the Taxonomy has the provided (Tag) values, slight difference where the former would return a 400 and the later would return a 404. I guess the end result is technically the same (the request fails), just thought this give more granularity in checks and errors.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The implementation in the Taxonomy.delete_tags is actually an incorrect one, since the simple value__in filter doesnt account for case-insensitve values, the current tests didn't catch it.
Actually, doing some tests, surprising the simple implementation works 😮, It might be because of the special custom case_insensitive_char_field
we use in the model? Since that is generally not the default behavior for django's charfields. In any case, that was a pleasant surprise! I went ahead and simplified the validation.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah lovely, I'm glad to hear that case-insensitive fields aren't so hard to interact with!
I'm still not convinced that the extra Serializer validation is needed, since the model does what it does and more. But I'll leave that to you & upstream's discretion.
68a7816
to
8e550d7
Compare
8e550d7
to
b2e5323
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
👍 Looks great to me @yusuf-musleh ! Ready for your eyes @bradenmacdonald .
Note: needs a version bump before merging.
- I tested this using the django devserver and the DRF REST API UI.
- I read through the code -- great tests!
-
I checked for accessibility issues - Includes documentation
- Commit structure follows OEP-0051
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Almost there! You don't have to do all of these as changes, just let me know your thoughts.
See also my suggestion on the edx-platform PR about changing how we use reverse
so that this API can be hosted in any namespace, not just oel_tagging
""" | ||
valid = Tag.objects.filter(value__iexact=value).exists() | ||
if not valid: | ||
raise serializers.ValidationError("Invalid `parent_tag_value` provided") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Personally I would put this validation in the Taxonomy.add_tag
method, where most other validation is anyways. That way it gets used for both the python API and REST API. Currently, only the REST API has this validation. You can leave it as-is though.
Removing the validation method(s) from these three serializers would also make the serializer code much more compact.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
After trying it out, it's definitely more concise and having the validations all in one place is cleaner, thanks @pomegranited and @bradenmacdonald for the suggestions! I updated it. b0e4b26
{ | ||
"tag": "Tag 1", | ||
"updated_tag_value": "Updated Tag Value" | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nit: I think it would be a bit more RESTful to put the old value in the URL and just use value
as the name of the new value field. Though since value
is a mutable ID it's not a huge deal. So no need to change for now.
What I would definitely recommend we do now is remove the PUT
example from the docstring. Because if we add the ability to change other fields like external_id
via this API endpoint in the future, anyone who is using PUT
to change the name will now be accidentally overwriting/deleting the external_id
, since PUT means "completely replace the tag with this new spec". But PATCH
explicitly means "update the fields I specify to the new values and leave others unchanged".
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I see, for sure, I removed the PUT
example as suggested, and kept the endpoint as is for now.
In addition to other PR requested changes: * Remove `PUT` method from docs string to avoid confusion with future changes * Replace `pk` with `id` in doc string to make it more RESTful
15f903c
to
b0e4b26
Compare
@bradenmacdonald Thanks for the review! I address/applied your suggestions, including the name space suggestion, I also bumped the version, it should hopefully be good to go now. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for those changes! This is good to merge, but I would like to see those duplicate checks removed first, unless there's some reason to keep them in that I'm missing.
@bradenmacdonald Thanks for the review! I've updated the PR to remove the duplicate checks, and the version bump is already included. |
@yusuf-musleh 🎉 Your pull request was merged! Please take a moment to answer a two question survey so we can improve your experience in the future. |
Description
This PR introduces the CRUD python and REST APIs for interacting with Taxonomy Tags. Creating, updating and deleting them.
Supporting Information
Related Tickets:
Testing Instructions
Make sure all the tests pass, and they cover all the cases.
Private-ref: FAL-3519